#pragma once

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <tuple>
#include <type_traits>

#include "component/third_party/gsl/gsl/gsl-lite.hpp"

#if __cplusplus == 201703L

namespace std {
using namespace gsl::std20;

template <class T>
using span = gsl::span<T>;

template <class T>
using remove_cvref_t = typename std::remove_cvref<T>::type;
}  // namespace std

#elif __cplusplus < 201703L  // #if __cplusplus == 201703L

#error C++17 is required!

#endif  // #elif __cplusplus < 201703L

namespace binio {
template <typename DataType, size_t kDataOffset = 0, typename = void>
class BufObj {};
}  // namespace binio

namespace component::middleware::util::binio {

#include "component/middleware/util/binio/component.h"

class CDataCommon {
 protected:
  CDataCommon() = default;

 private:
  CDataCommon(CDataCommon&&) = default;
  CDataCommon(const CDataCommon&) = default;
  CDataCommon& operator=(CDataCommon&&) = default;
  CDataCommon& operator=(const CDataCommon&) = default;
};

template <class CData, bool kHasBufData>
class CDataBase : public CDataCommon {
 public:
  static constexpr bool HasBufData() { return kHasBufData; }
  static constexpr std::string_view GetTypeName() { return CData::DoGetTypeName(); }
  template <bool kHasBufData_ = HasBufData()>
  constexpr size_t GetBufDataSize(std::enable_if_t<kHasBufData_ == true>* = nullptr) const {
    return static_cast<const CData*>(this)->DoGetBufDataSize();
  }
  template <bool kHasBufData_ = HasBufData()>
  constexpr size_t GetBufDataSize(std::enable_if_t<kHasBufData_ == false>* = nullptr) const {
    return 0;
  }
  constexpr auto GetValue() const { return static_cast<const CData*>(this)->DoGetValue(); }
  template <typename ValueType, bool kHasBufData_ = HasBufData()>
  void SetValue(const ValueType& value, char*& data_buf,
                std::enable_if_t<kHasBufData_ == true>* = nullptr) {
    static_cast<CData*>(this)->DoSetValue(value, data_buf);
  }
  template <typename ValueType, bool kHasBufData_ = HasBufData()>
  void SetValue(const ValueType& value, std::enable_if_t<kHasBufData_ == false>* = nullptr) {
    static_cast<CData*>(this)->DoSetValue(value);
  }

  static CData& Create(char*& buf) {
    auto* obj = new (buf) CData();
    buf += sizeof(*obj);
    return *obj;
  }
  template <typename ValueType>
  static CData& Create(char*& buf, const ValueType& value) {
    auto& obj = Create(buf);
    if constexpr (HasBufData()) {
      obj.SetValue(value, buf);
    } else {
      obj.SetValue(value);
    }
    return obj;
  }

 protected:
  CDataBase() {
    static_assert(std::is_trivially_copyable_v<CData> && std::is_standard_layout_v<CData>);
  }
};

template <typename DataType, typename = void>
struct CDataConvert {
  using Type = void;
};

template <typename DataType>
struct CDataConvert<
    DataType,
    std::enable_if_t<std::is_base_of_v<CDataBase<DataType, DataType::HasBufData()>, DataType>>> {
  using Type = DataType;
};

template <class CPrimData, class PrimType>
class CPrimDataBase : public CDataBase<CPrimData, false> {
 public:
  using Base = CDataBase<CPrimData, false>;
  using PrimitiveType = PrimType;

 protected:
  PrimType value_ = 0;

  constexpr auto DoGetValue() const { return value_; }
  void DoSetValue(const PrimType& value) { value_ = value; }
};

#define BINIO_DEF_PRIM_DATA(NAME, TYPE)                                              \
  class NAME : public CPrimDataBase<NAME, TYPE> {                                    \
   protected:                                                                        \
    friend Base;                                                                     \
    NAME() = default;                                                                \
    static std::string_view DoGetTypeName() {                                        \
      static std::string type_name(std::string(kComponentName) + (".binio." #NAME)); \
      return type_name;                                                              \
    }                                                                                \
  };                                                                                 \
  template <>                                                                        \
  struct CDataConvert<TYPE, void> {                                                  \
    using Type = NAME;                                                               \
  }

BINIO_DEF_PRIM_DATA(CByte, std::byte);
BINIO_DEF_PRIM_DATA(CChar, signed char);
BINIO_DEF_PRIM_DATA(CUChar, unsigned char);
BINIO_DEF_PRIM_DATA(CShort, signed short);
BINIO_DEF_PRIM_DATA(CUShort, unsigned short);
BINIO_DEF_PRIM_DATA(CInt, signed int);
BINIO_DEF_PRIM_DATA(CUInt, unsigned int);
BINIO_DEF_PRIM_DATA(CLong, signed long);
BINIO_DEF_PRIM_DATA(CULong, unsigned long);
BINIO_DEF_PRIM_DATA(CLongLong, signed long long);
BINIO_DEF_PRIM_DATA(CULongLong, unsigned long long);
BINIO_DEF_PRIM_DATA(CFloat, float);
BINIO_DEF_PRIM_DATA(CDouble, double);
BINIO_DEF_PRIM_DATA(CLongDouble, long double);

using CInt8 = CDataConvert<std::int8_t>::Type;
using CUInt8 = CDataConvert<std::uint8_t>::Type;
using CInt16 = CDataConvert<std::int16_t>::Type;
using CUInt16 = CDataConvert<std::uint16_t>::Type;
using CInt32 = CDataConvert<std::int32_t>::Type;
using CUInt32 = CDataConvert<std::uint32_t>::Type;
using CInt64 = CDataConvert<std::int64_t>::Type;
using CUInt64 = CDataConvert<std::uint64_t>::Type;
using CSize = CDataConvert<std::size_t>::Type;

class CString : public CDataBase<CString, true> {
 public:
  using Base = CDataBase<CString, true>;
  using ElementType = char;

  constexpr const char* GetData() const { return data_buf_ + offset_; }
  constexpr char* GetData() { return data_buf_ + offset_; }
  constexpr size_t GetLength() const { return length_; }
  constexpr const ElementType& operator[](size_t idx) const { return GetData()[idx]; }
  constexpr ElementType& operator[](size_t idx) { return GetData()[idx]; }

 protected:
  friend Base;
  CString() = default;

  static std::string_view DoGetTypeName() {
    static std::string type_name(std::string(kComponentName) + ".binio.CString");
    return type_name;
  }
  constexpr size_t DoGetBufDataSize() const { return length_; }
  constexpr auto DoGetValue() const {
    return std::string_view(this->GetData(), this->GetBufDataSize());
  }
  void DoSetValue(std::string_view value, char*& data_buf) {
    if (length_ >= value.size()) {
      std::move(value.begin(), value.end(), data_buf_ + offset_);
    } else {
      offset_ = data_buf - data_buf_;
      std::move(value.begin(), value.end(), data_buf);
      data_buf += value.size();
    }
    length_ = value.size();
  }

  size_t offset_ = 0, length_ = 0;
  char data_buf_[0];
};

template <class ElemType, size_t N>
class CArray : public CDataBase<CArray<ElemType, N>, ElemType::HasBufData()> {
  static_assert(std::is_base_of_v<CDataBase<ElemType, ElemType::HasBufData()>, ElemType>);

 public:
  using Base = CDataBase<CArray<ElemType, N>, ElemType::HasBufData()>;
  using ElementType = ElemType;
  static constexpr auto kNumElements = N;

  constexpr const ElemType* GetData() const { return reinterpret_cast<ElemType*>(val_buf_); }
  constexpr ElemType* GetData() { return reinterpret_cast<ElemType*>(val_buf_); }
  constexpr size_t GetLength() const { return kNumElements; }
  constexpr const ElementType& operator[](size_t idx) const { return GetData()[idx]; }
  constexpr ElementType& operator[](size_t idx) { return GetData()[idx]; }

 protected:
  friend Base;

  char val_buf_[kNumElements * sizeof(ElemType)] = {};

  static std::string_view DoGetTypeName() {
    static std::string type_name(std::string(kComponentName) + ".binio.CArray(" +
                                 std::string(ElemType::GetTypeName()) + ", " +
                                 std::to_string(kNumElements) + ")");
    return type_name;
  }
  constexpr size_t DoGetBufDataSize() const {
    if constexpr (ElementType::HasBufData()) {
      size_t size = 0;
      auto* elem = reinterpret_cast<const ElemType*>(val_buf_);
      for (size_t _ = 0; _ < kNumElements; ++_, ++elem) {
        size += elem->GetBufDataSize();
      }
      return size;
    }
    return 0;
  }
  constexpr auto DoGetValue() const {
    return std::span<const ElemType>(reinterpret_cast<const ElemType*>(val_buf_), kNumElements);
  }
  template <typename ElemValType, bool kHasBufData_ = ElementType::HasBufData()>
  void DoSetValue(std::span<ElemValType> value, char*& data_buf,
                  std::enable_if_t<kHasBufData_ == true>* = nullptr) {
    auto len = std::min(value.size(), kNumElements);
    auto* elem = reinterpret_cast<ElemType*>(val_buf_);
    for (size_t idx = 0; idx < len; ++idx, ++elem) {
      elem->SetValue(value[idx], data_buf);
    }
  }
  template <typename ElemValType, bool kHasBufData_ = ElementType::HasBufData()>
  void DoSetValue(std::span<ElemValType> value,
                  std::enable_if_t<kHasBufData_ == false>* = nullptr) {
    auto* src_bgn = reinterpret_cast<const char*>(value.data());
    std::move(src_bgn, src_bgn + std::min(kNumElements, value.size()) * sizeof(ElemType), val_buf_);
  }
};

template <class DataType>
struct IsCArray : public std::false_type {};

template <class ElemType, size_t N>
struct IsCArray<CArray<ElemType, N>> : public std::true_type {};

template <class ElemType, size_t N>
struct CDataConvert<std::array<ElemType, N>,
                    std::enable_if_t<CDataConvert<ElemType>::Type::HasBufData() == false>> {
  using Type = CArray<typename CDataConvert<ElemType>::Type, N>;
};

template <class ElemType, size_t N>
struct CDataConvert<ElemType[N],
                    std::enable_if_t<CDataConvert<ElemType>::Type::HasBufData() == false>> {
  using Type = CArray<typename CDataConvert<ElemType>::Type, N>;
};

template <class ElemType>
class CVector : public CDataBase<CVector<ElemType>, true> {
  static_assert(std::is_base_of_v<CDataBase<ElemType, ElemType::HasBufData()>, ElemType>);

 public:
  using Base = CDataBase<CVector<ElemType>, true>;
  using ElementType = ElemType;

  constexpr const ElemType* GetData() const {
    return reinterpret_cast<const ElemType*>(data_buf_ + offset_);
  }
  constexpr ElemType* GetData() { return reinterpret_cast<ElemType*>(data_buf_ + offset_); }
  constexpr size_t GetLength() const { return length_; }
  constexpr const ElementType& operator[](size_t idx) const { return GetData()[idx]; }
  constexpr ElementType& operator[](size_t idx) { return GetData()[idx]; }

 protected:
  friend Base;

  size_t offset_ = 0, length_ = 0;
  char data_buf_[0];

  static std::string_view DoGetTypeName() {
    static std::string type_name(std::string(kComponentName) + ".binio.CVector(" +
                                 std::string(ElemType::GetTypeName()) + ")");
    return type_name;
  }
  constexpr size_t DoGetBufDataSize() const {
    size_t size = length_ * sizeof(ElemType);
    if constexpr (ElementType::HasBufData()) {
      auto* values = GetData();
      for (size_t idx = 0; idx < length_; ++idx) {
        size += values[idx].GetBufDataSize();
      }
    }
    return size;
  }
  constexpr auto DoGetValue() const { return std::span<const ElemType>(GetData(), length_); }
  template <typename ElemValType, bool kHasBufData_ = ElementType::HasBufData()>
  void DoSetValue(std::span<ElemValType> value, char*& data_buf,
                  std::enable_if_t<kHasBufData_ == true>* = nullptr) {
    if (length_ >= value.size()) {
      length_ = value.size();
      auto* element = GetData();
      std::for_each(value.begin(), value.end(),
                    [&element, &data_buf](ElemValType& elem_val) mutable {
                      element->SetValue(elem_val, data_buf);
                      ++element;
                    });
    } else {
      auto* elem_data_buf = data_buf + (length_ = value.size()) * sizeof(ElemType);
      offset_ = data_buf - data_buf_;
      auto* element = reinterpret_cast<ElemType*>(data_buf);
      std::for_each(value.begin(), value.end(),
                    [&element, &elem_data_buf](ElemValType& elem_val) mutable {
                      element->SetValue(elem_val, elem_data_buf);
                      ++element;
                    });
      data_buf = elem_data_buf;
    }
  }
  template <typename ElemValType, bool kHasBufData_ = ElementType::HasBufData()>
  void DoSetValue(std::span<ElemValType> value, char*& data_buf,
                  std::enable_if_t<kHasBufData_ == false>* = nullptr) {
    auto* src_bgn = reinterpret_cast<const char*>(value.data());
    auto buf_len = value.size() * sizeof(ElemType);
    if (length_ >= value.size()) {
      std::move(src_bgn, src_bgn + buf_len, data_buf_ + offset_);
    } else {
      offset_ = data_buf - data_buf_;
      std::move(src_bgn, src_bgn + buf_len, data_buf);
      data_buf += buf_len;
    }
    length_ = value.size();
  }
};

template <class DataType>
struct IsCVector : public std::false_type {};

template <class ElemType>
struct IsCVector<CVector<ElemType>> : public std::true_type {};

struct BufObjCommon {
  struct BufHdr {
    size_t len;
    char body[0];
  };

 protected:
  template <class BufObj>
  constexpr auto Create(BufHdr& buf_hdr, char* data_start) {
    return BufObj(buf_hdr, data_start);
  }
};

template <class BufObj, typename DataType, size_t kDataOffset = 0>
class BufObjBase : public BufObjCommon {
 public:
  BufObjBase(char* buf)
      : buf_hdr_(*new(buf) BufHdr{.len = sizeof(DataType)}), data_start_(buf_hdr_.body) {
    std::fill(this->GetBuf(), this->GetBuf() + sizeof(DataType), 0);
  }

  static constexpr std::string_view GetTypeName() { return BufObj::DoGetTypeName(); }

 protected:
  BufHdr& buf_hdr_;
  char* data_start_;

  BufObjBase(BufHdr& buf_hdr, char* data_start) : buf_hdr_(buf_hdr), data_start_(data_start) {}

  constexpr auto& GetBufferedData() const {
    return *reinterpret_cast<DataType*>(data_start_ + kDataOffset);
  }
  constexpr auto* GetBuf() const { return buf_hdr_.body + buf_hdr_.len; }
  void SetBuf(char* buf) { buf_hdr_.len = buf - buf_hdr_.body; }
};

template <typename DataType, size_t kDataOffset = 0>
class BasicBufObj : public BufObjBase<BasicBufObj<DataType, kDataOffset>, DataType, kDataOffset> {
 public:
  using Base = BufObjBase<BasicBufObj, DataType, kDataOffset>;
  using CData = typename CDataConvert<DataType>::Type;
  using CDataBaseType = CDataBase<CData, CData::HasBufData()>;
  static_assert(std::is_same_v<CData, void> == false);
  static_assert(std::is_base_of_v<CDataBaseType, CData>);

  BasicBufObj(char* buf) : Base(buf) {}
  template <typename ValueType>
  BasicBufObj(char* buf, const ValueType& value) : Base(buf) {
    this->SetValue(value);
  }

  constexpr auto GetValue() const {
    return reinterpret_cast<const CData&>(this->GetCData()).GetValue();
  }
  template <typename ValueType>
  void SetValue(const ValueType& value) {
    if constexpr (CDataBaseType::HasBufData()) {
      auto* buf = this->GetBuf();
      this->GetCData().SetValue(value, buf);
      this->SetBuf(buf);
    } else {
      this->GetCData().SetValue(value);
    }
  }

 protected:
  friend Base;

  BasicBufObj(typename Base::BufHdr& buf_hdr, char* data_start) : Base(buf_hdr, data_start) {}
  constexpr auto& GetCData() { return reinterpret_cast<CDataBaseType&>(this->GetBufferedData()); }
  constexpr const auto& GetCData() const {
    return reinterpret_cast<const CDataBaseType&>(this->GetBufferedData());
  }

  static constexpr std::string_view DoGetTypeName() { return CDataBaseType::GetTypeName(); }
};

template <class BufObj_, typename DataType_, size_t kDataOffset = 0>
class CompoundBufObj : public BufObjBase<BufObj_, DataType_, kDataOffset> {
  template <typename Field>
  struct FieldInfo {
    using CDataType = typename CDataConvert<typename Field::DataType>::Type;
    using BufObjType = ::binio::BufObj<
        std::conditional_t<std::is_same_v<CDataType, void>, typename Field::DataType, CDataType>,
        Field::kOffset>;
    static constexpr std::string_view GetFieldName() { return Field::GetFieldName(); }
  };

 public:
  struct FieldBase {
    using DataType = void;
    static constexpr auto kOffset = -1;
    static constexpr std::string_view GetFieldName() { return "Unknown"; }
  };
  using Base = BufObjBase<BufObj_, DataType_, kDataOffset>;
  using Base::Base;

  template <typename Field>
  auto& GetField(const Field&) {
    static_assert(std::is_base_of_v<FieldBase, Field>);
    return *reinterpret_cast<typename FieldInfo<Field>::BufObjType*>(this);
  }
  template <typename Field>
  const auto& GetField(const Field&) const {
    static_assert(std::is_base_of_v<FieldBase, Field>);
    return *reinterpret_cast<const typename FieldInfo<Field>::BufObjType*>(this);
  }

  std::string GetTypeDesc() {
    return "class " + std::string(Base::GetTypeName().substr(Base::GetTypeName().rfind('.') + 1)) +
           "(\n    middleware__util__binio.binio.CompoundCData,\n    " +
           this->GetTypeDescImpl<std::tuple_size_v<typename BufObj_::FieldTuple> - 2>() +
           "\n):\n    pass";
  }

 protected:
  CompoundBufObj(typename Base::BufHdr& buf_hdr, char* data_start) : Base(buf_hdr, data_start) {}

  static std::string_view CleanCompPrefix(std::string_view type_name,
                                          std::string_view comp_prefix) {
    return (type_name.substr(0, comp_prefix.size()) == comp_prefix)
               ? type_name.substr(comp_prefix.size())
               : type_name;
  }

  template <size_t kFieldIdx>
  static constexpr std::string GetTypeDescImpl() {
    using Field = std::tuple_element_t<kFieldIdx, typename BufObj_::FieldTuple>;
    static_assert(std::is_base_of_v<FieldBase, Field>);
    std::string_view comp_prefix(Base::GetTypeName().substr(0, Base::GetTypeName().rfind('.') + 1));
    if constexpr (kFieldIdx == 0) {
      return std::string(FieldInfo<Field>::GetFieldName()) + " = " +
             std::string(CleanCompPrefix(FieldInfo<Field>::BufObjType::GetTypeName(), comp_prefix));
    } else {
      using PrevField = std::tuple_element_t<kFieldIdx - 1, typename BufObj_::FieldTuple>;
      constexpr auto kPadSize =
          Field::kOffset - PrevField::kOffset - sizeof(typename PrevField::DataType);
      if constexpr (kPadSize > 0) {
        return GetTypeDescImpl<kFieldIdx - 1>() + ",\n    " + "_pad__" +
               std::string(FieldInfo<PrevField>::GetFieldName()) +
               " = middleware__util__binio.binio.CArray(middleware__util__binio.binio.CUChar, " +
               std::to_string(kPadSize) + "),\n    " +
               std::string(FieldInfo<Field>::GetFieldName()) + " = " +
               std::string(
                   CleanCompPrefix(FieldInfo<Field>::BufObjType::GetTypeName(), comp_prefix));
      } else {
        return GetTypeDescImpl<kFieldIdx - 1>() + ",\n    " +
               std::string(FieldInfo<Field>::GetFieldName()) + " = " +
               std::string(
                   CleanCompPrefix(FieldInfo<Field>::BufObjType::GetTypeName(), comp_prefix));
      }
    }
  }
};

}  // namespace component::middleware::util::binio

namespace binio {

template <typename DataType, size_t kDataOffset>
class BufObj<DataType, kDataOffset,
             std::enable_if_t<std::is_base_of_v<
                 ::component::middleware::util::binio::CPrimDataBase<
                     typename ::component::middleware::util::binio::CDataConvert<DataType>::Type,
                     typename ::component::middleware::util::binio::CDataConvert<
                         DataType>::Type::PrimitiveType>,
                 typename ::component::middleware::util::binio::CDataConvert<DataType>::Type>>>
    : public ::component::middleware::util::binio::BasicBufObj<DataType, kDataOffset> {
 public:
  using Base = ::component::middleware::util::binio::BasicBufObj<DataType, kDataOffset>;
  using Base::Base;

 protected:
  friend ::component::middleware::util::binio::BufObjCommon;

  BufObj(typename Base::BufHdr& buf_hdr, char* data_start) : Base(buf_hdr, data_start) {}
};

template <typename DataType, size_t kDataOffset>
class BufObj<
    DataType, kDataOffset,
    std::enable_if_t<std::is_same_v<::component::middleware::util::binio::CString, DataType> ||
                     ::component::middleware::util::binio::IsCArray<DataType>::value ||
                     ::component::middleware::util::binio::IsCVector<DataType>::value>>
    : public ::component::middleware::util::binio::BasicBufObj<DataType, kDataOffset> {
 public:
  using Base = ::component::middleware::util::binio::BasicBufObj<DataType, kDataOffset>;
  using ElementType =
      typename ::component::middleware::util::binio::CDataConvert<DataType>::Type::ElementType;
  using ElementObj = BufObj<ElementType>;
  using Base::Base;

  constexpr const ElementType* GetData() const { return this->GetCData().GetData(); }
  constexpr ElementType* GetData() { return this->GetCData().GetData(); }
  constexpr size_t GetLength() const { return this->GetCData().GetLength(); }
  constexpr ElementObj operator[](size_t idx) {
    return ::component::middleware::util::binio::BufObjCommon::Create<ElementObj>(
        Base::buf_hdr_, reinterpret_cast<char*>(&(this->Base::GetBufferedData()[idx])));
  }

 protected:
  friend ::component::middleware::util::binio::BufObjCommon;

  BufObj(typename Base::BufHdr& buf_hdr, char* data_start) : Base(buf_hdr, data_start) {}
  constexpr auto& GetCData() { return reinterpret_cast<DataType&>(this->Base::GetBufferedData()); }
};

}  // namespace binio

#define BINIO_DEF_FIELD(FIELD_NAME)                                          \
  struct Field_##FIELD_NAME : public Base::FieldBase {                       \
    using DataType = decltype(static_cast<StructType*>(0)->FIELD_NAME);      \
    static constexpr auto kOffset = offsetof(StructType, FIELD_NAME);        \
    static constexpr std::string_view GetFieldName() { return #FIELD_NAME; } \
  };                                                                         \
  auto& FIELD_NAME() { return this->GetField(Field_##FIELD_NAME()); }        \
  const auto& FIELD_NAME() const { return this->GetField(Field_##FIELD_NAME()); }

#define BINIO_FIELD_CONCAT(FIELD_NAME) Field_##FIELD_NAME,

#define BINIO_DEF_STRUCT_BUFOBJ(COMP_NS, NAME, BINIO_LIST_FIELDS)                                \
  template <size_t kDataOffset>                                                                  \
  class BufObj<COMP_NS::NAME, kDataOffset>                                                       \
      : public ::component::middleware::util::binio::CompoundBufObj<                             \
            BufObj<COMP_NS::NAME, kDataOffset>, COMP_NS::NAME, kDataOffset> {                    \
   public:                                                                                       \
    using Base =                                                                                 \
        ::component::middleware::util::binio::CompoundBufObj<BufObj<COMP_NS::NAME, kDataOffset>, \
                                                             COMP_NS::NAME, kDataOffset>;        \
    using Base::Base;                                                                            \
    using StructType = COMP_NS::NAME;                                                            \
                                                                                                 \
    BINIO_LIST_FIELDS(BINIO_DEF_FIELD);                                                          \
                                                                                                 \
   protected:                                                                                    \
    friend ::component::middleware::util::binio::BufObjCommon;                                   \
    friend Base;                                                                                 \
    friend typename Base::Base;                                                                  \
    using FieldTuple = std::tuple<BINIO_LIST_FIELDS(BINIO_FIELD_CONCAT) void>;                   \
                                                                                                 \
    static std::string_view DoGetTypeName() {                                                    \
      static std::string type_name(std::string(COMP_NS::kComponentName) + (".binio." #NAME));    \
      return type_name;                                                                          \
    }                                                                                            \
  }
